""" Widgets that contain other widgets.

    This is where both Window and Desktop are defined.
"""

import os
from operator import attrgetter

from pygame import (
    Surface,
    Rect,
    MOUSEBUTTONDOWN,
    MOUSEBUTTONUP,
    MOUSEMOTION,
    VIDEORESIZE
)

import sbak.draw
from sbak.draw import (
    rect_ as draw_rect_,
    text as draw_text,
    fix_color
)
from sbak.event import (
    Event,
    Responder,
    Forwarder,
    EventManager,
    CONTROLEVENT,
    CE_CYCLE,
    CE_DISPLAY,
    CE_REFRESH,
)
from sbak.video import (
    set_mode as video_set_mode,
    get_screen as video_get_screen,
    update as video_update
)
from sbak.widget import (
    DEFAULT_WINDOW_REGION,
    AbstractWidget,
    AbstractContainer
)

class Window(AbstractContainer, EventManager):
    """ A container widget that has its own drawing surface.
    
        win = Window(region = None, widgets = ())
        
        ~~~ Constructor Arguments ~~~
        
        region:     The region the window occupies in its parent. The
                    default is the value of
                    `sbak.widget.DEFAULT_WINDOW_REGION`.
        
        widgets:    Widgets to put into the window.
    
        surface:    Drawing surface for widgets in the window. If None
                    is given (default), a new, empty surface is created
                    with size equal to the region above.
                    
        ~~~ Description ~~~
        
        A window is a widget that contains other widgets and provides
        a drawing surface other than the screen as well as a localized
        region. In many ways, a window is like a miniature Desktop that
        exists within the "true" desktop.
        
        Windows build from AbstractContainer, but add the fact that a
        window has its own unique surface rather than a subsurface of
        the parent. This is acknowledged by the fact that moving a
        window does not cause a redraw for its children, and other
        behaviors being simplified.
        
        The window implements handlers for the MOUSEBUTTONUP and
        MOUSEBUTTONDOWN events called, respectively,
        `handle_mousebuttonup` and `handle_mousebuttondown`, which
        prevent these events from getting passed to siblings that
        appear underneith the window whenever the window is a
        direct child of a desktop.
    """
    
    tag = 'window'
    
    def __init__(self,  region = DEFAULT_WINDOW_REGION,
                        widgets = (),
                        surface = None, id = None):
        EventManager.__init__(self, widgets, id = id)
        AbstractContainer.__init__(self, region = region)
        
        if surface is None:
            self._surface = Surface(self.region.size)
        else:
            self._surface = surface
            
        # Set the hooks we need.
        hooks = {
            MOUSEBUTTONDOWN : self.handle_mousebuttondown,
            MOUSEBUTTONUP : self.handle_mousebuttonup
        }
        for type, handler in hooks.iteritems():
            self.hook(handler, type)


    _doc_surface =\
        """ The drawing surface for widgets in the window.
            
            This is currently read-only.
        """
    _fget_surface = attrgetter('_surface')
    
    
    _doc_region =\
        """ Region the window occupies in the parent.
        
            If the region is changed, one of two things may happen
            depending on whether or not the size of the new region is
            equal to the size of the old region:
            
            1. If the two regions' sizes are equal, the parent is
            smudged in both regions, but the window and its children
            are not.
            
            2.  If the two region's sizes are NOT equal, the entire
            container is smudged, and each child is smudged in a
            region that is translated for each child.
        """
    
    def _fset_region(self, region):
        super(Window,self)._fset_region(region)
        
        # Get the size component of the new region
        size = region[2:4]
        
        # If the new size is different than the old size...
        if self.region.size != size:
            # Create a new surface for the window.
            surface = Surface(size)
            self._surface = surface
    
    def resize(self, size):
        """ TODO: Test me! """
        super(Window,self).resize(size)
        
        # Create a new surface for the window.
        surface = Surface(size)
        self._surface = surface
    
    def handle_mousebuttondown(self, e):
        if self.work_region_abs.collidepoint(e.pos):
            return -1
    
    def handle_mousebuttonup(self, e):
        if self.work_region_abs.collidepoint(e.pos):
            return -1

class Desktop(Window):
    """ A window that uses the SDL screen as its surface.
    
        d = Desktop(
            res=None,
            bpp=None,
            full=None,
            center=False,
            resizable=False,
            noframe=False,
            widgets=(),
            
            id = None
        )
        
        ~~~ Constructor Arguments ~~~
        
        res:    Screen resolution as ordered pair. This will be the
                size of the window or resolution of the screen
                depending on whether the game runs in windowed mode or
                full screen mode, respectively. Nearly any size of
                window will work, but resolutions supported will
                depend on the graphics card and monitor of the
                computer. The default resolution is %(DEFAULT_RES)s.
        
        bpp:    The number of bits per pixel of the screen surface.
                Supported values will depend on the system, but
                typically 8, 16, 24, or 32 will work. Default %(DEFAULT_BPP)i.
        
        full:   Whether or not to display in full screen mode.
                If false, the display is a window. If true, it is
                the whole screen. If this is true the screen resolution
                will be changed to the value of `res`. If that
                resolution is not supported an exception will occur.
                Default %(DEFAULT_FULL)s.
        
        center: Whether or not to center the window (screen) when it is
                first created. The effect of this will not be visible
                unless `full` is False. Also, the window can still be
                moved even if it is initially centered. Default False.
        
        resizable:
                If True, the window will be resizable, and events will
                be generated when the user resizes the window. (See
                the Pygame event documentation.)
        
        boborder:
                If True, thw window will not have a border. Be aware
                that this means there will not be a close button
                either, do you will have to utilize another form of
                input to make the app exit.
        
        widgets:
                A sequence of widgets to put into the desktop
                initially.
        
        ~~~ Description ~~~
        
        The Desktop is a window that uses the screen as its surface.
        Consequently, most operations performed on the desktop will
        effect the screen.
        
        Creating a desktop will set the screen mode. For this reason,
        the desktop constructor accepts all the same arguments as
        `sbak.video.set_mode`, as well as several new arguments
        specific to desktop objects. You can set the initial size of
        the desktop, but not the overall region, because the desktop's
        position must always be (0,0) so that it stays lined up with
        the SDL screen window.
        
        Be aware that if you pass True for `full` or `noborder` you
        will not be able to close the SDL window using the close button
        because it will not be present. Therefore, make sure there are
        other ways to close the window from within in.
        
        All arguments except for `res` and `windows` are remembered in
        attributes of the same names. When the desktop is resized using
        the `.resize` method, the screen mode will be changed using
        the settings stored in those attributes at the time of
        resizing.
        
        WARNING: Having multiple desktops in the application tree at
        the same time will probably cause problems because they will
        all want to occupy the entire screen and use the screen
        surface at once.
        
        ~~~ Notes ~~~
        
        *   If the desktop has no border then you will not be able to
            move, resize, minimize, or close the SDL screen window
            using the border controls, so make sure your application
            provides an alternate means of doing these things.
    """
    
    def __init__(self,  res=None,
                        bpp=None,
                        full=None,
                        center=False,
                        resizable=False,
                        noframe=False,
                        widgets=(),
                        id = None ):
        
        screen = video_set_mode(
            res, bpp, full, center, resizable, noframe
        )
        
        Window.__init__(self,
            region = Rect((0,0), screen.get_size()),
            widgets = widgets,
            surface = screen,
            id = id
        )
        
        # Remember flags set here so that they can be applied again when we
        # resize.
        self.bpp = bpp
        self.full = full
        self.center = center
        self.resizable = resizable
        self.noframe = noframe
        
        # Hook all our handlers.
        hooks = {
            CE_DISPLAY : self.handle_display,
        }
        for type, handler in hooks.iteritems():
            self.hook(handler, CONTROLEVENT, control = type)
        self.hook(self.handle_videoresize, VIDEORESIZE)
    
    def get_desktop(self):
        """ Returns self.
        
            Widgets under the desktop use this to find the desktop they
            are in.
        """
        return self

    def _fset_region(self, val):
        raise TypeError("Desktop region cannot be changed.")

    def move(self, pos):
        raise TypeError("Desktop cannot be moved.")

    def resize(self, size):
        """ Resize the desktop, thus changing the screen mode.
        
            This will change the screen mode so that the desktop is the
            new size. This is done by changing the screen mode.
            
            FIXME: Resizing causes full-screen flickering.
        """
        self._surface = video_set_mode(
            res = size,
            bpp = self.bpp,
            full = self.full,
            center = self.center,
            resizable = self.resizable,
            noframe = self.noframe
        )
        self._region = Rect((0,0), self._surface.get_size())
        self._update_regions()
    @property
    def region_abs(self):
        return self.region
    
    def notify(self, e):
        """ A modified form of EventManager.notify for desktops.
        
            This version of .notify works like EventManager's version,
            except that it processes each child in reverse order. In
            other words, the FIRST child of the desktop receives events
            LAST, and the LAST child receives them FIRST.
        """
        ret = Responder.notify(self, e)
        if ret == -1 or not ret:
            for child in reversed(self[:]):
                ret2 = child.notify(e)
                if ret2:
                        return ret2
            return ret
            
        return ret
    
    def draw_internal(self, dest, region):
        """ Draws a desktop box.
        
            This actually just fills the indicated region with the
            value of the "desktop-color" style key.
        """
        dest.fill((128,128,128), region)
    
    def handle_display(self, e):
        """ CE_DISPLAY control event handler for the Desktop.
        
            Calls `self.display` with the values indicated by the
            event.
        """
        
        if e.region is not None:
            self.display(e.surface, e.offset, e.region)
        else:
            self.display(e.surface, e.offset)

    def handle_videoresize(self, e):
        """ VIDEORESIZE event handler for Desktop.
        
            This will resize the desktop so that it matches the new
            size as indiacted by the event information. Display flags
            are taken from the desktop attributes `full`, `noframe`,
            `resizable`, etc.
        """
        self.resize(e.size)

class Form(Window):
    """ A window with a titlebar that can be used to move the window.
    
        tw = Form(
            region = DEFAULT_WINDOW_REGION,
            title = "",
            widgets = (),
            id = None
        )
    
        ~~~ Constructor Arguments ~~~
        
        region:     The region the window will cover in the parent. The
                    default is the value of
                    `widget.DEFAULT_WINDOW_REGION`.
        
        title:      The text of the titlebar. The default is the empty
                    string, "".
                    
        widgets:    Widgets to put into the window initially. No
                    widgets are put in by default.
        
        ~~~ Description ~~~
        
        Form enhances the Window class by adding a strip of
        color with text in it, called a "titlebar," to the top of the
        window. The user can click and drag the titlebar to move the
        form around. Movement will trigger "smudging," causing the
        objects "behind" the form to be redrawn.
        
        The form hooks handlers for the MOUSEBUTTONDOWN,
        MOUSEBUTTONUP, and MOUSEMOVE events. These are the methods
        `handle_mousedown`, `handle_mouseup`, and `handle_mousemove`
        methods, respectively.
    
    """
    
    def __init__(self, region=DEFAULT_WINDOW_REGION, text="", widgets=(),
                    id = id):
        Window.__init__(self, region = region, widgets = (), id = id)
        
        self._text = text
        
        # Flag to determine internally whether or not the window is moving.
        self._moving = False
        
        # Set the hooks we need.
        hooks = {
            MOUSEBUTTONDOWN : self.handle_mousebuttondown,
            MOUSEBUTTONUP : self.handle_mousebuttonup,
            MOUSEMOTION : self.handle_mousemotion
        }
        for type, handler in hooks.iteritems():
            self.hook(handler, type)
    
    def set_text(self, text):
        self._text = text
    text = property(
        fget = attrgetter('_text'),
        fset = set_text,
        doc =
        """ The title text that appears at the top of the form.
            
            Changing this will smudge the form in the region of the
            titlebar, causing a redraw of that region.
        """
    )
    
    def draw_internal(self, dest, region):
        """ Draws a widget box with a titlebar containing text.
        
            The titlebar styling comes from the "title-color",
            "title-text-color", "title-height", and "title-text-pos"
            style keys. The text comes from `self.text`.
        """
        super(Form,self).draw_internal(dest,region)
        sbak.draw.text(dest, region.move(2,2),
                       text=self.text, fgcolor="#000000")
    
    def handle_mousebuttondown(self, e):
        """ MOUSEBUTTONDOWN event handler for Form.
        
            If the pointer was pressed while it was in the form's
            region on the screen, the form is moved to the end of
            the parent's child list so that it will receive events
            before all other siblings, and so that it appears on top
            of siblings (assuming the form is in a desktop).
        
            If any mouse button is pressed when the pointer is on the
            titlebar of the form, the form enters "moving mode",
            and True is returned, preventing the event from propogating
            to children or siblings.
        """
        # Determine the window's position on the screen.
        region_abs = self.region_abs
        
        # If the user clicked in the window's screen region, respond.
        if region_abs.collidepoint(e.pos):
        
            # Move the window to the end of the parent's child list.
            # FIXME: This could have side-effects if the parent overrides the
            # node methods.
            parent = self.parent
            parent.remove(self)
            parent.append(self)
            
            # Determine where the titlebar is on the screen
            tregion = Rect(region_abs)
            tregion.height = 24
            
            # If the mouse was pressed on the titlebar region, start moving.
            if tregion.collidepoint(e.pos):
                
                # Enter "moving mode."
                self._moving = True
                
                # Prevent this event from being processed any further.
                return True
        
            # Allow the current event to be forwarded to window's children,
            # but not to siblings.
            return -1
    
    def handle_mousebuttonup(self, e):
        """ MOUSEBUTTONUP event handler for Form.
        
            Exits "moving mode."
        """
        self._moving = False

    def handle_mousemotion(self, e):
        """ MOUSEMOTION event handler for Form.
        
            Moves the form by the relative mouse motion whenever the
            form is in "moving mode."
        """
        if self._moving:
            self.move(e.rel)
